[FaaS] はじめてのfn #fnproject
こむろ@事業開発部です。訳あってFaaSのサービス実装について調べています。
はじめに
fnは Function as a Service(FaaS) の実行基盤を構築するサービス実装の一つです。Oracleが開発を主導しており、現在OSSでの開発が進められています。
FaaSの基本的な動作としては、Functionを呼び出すごとにContainerが起動され、仕事が終わったら一定の生存期間後に縮退、大きなComputer Resourceを複数のContainer(実態はFunction)で共有しながら待機時間を減らし効率の良い処理を目指します。
おや?これはAWS Lambdaなのでは?
LambdaもFaaSの一つになります。
違いはComputer Resourceの管理、運用がAWS側の管理下にあり、我々利用者側の制御管轄外であることです。当然ながら自前で管理する他のFaaSサービス実装は、それぞれComputer Resourceの管理・運用についても自分たちで制御しなければなりません。
しかし反面、細かい制御を行うことができるというところが大きい違いになりそうです。また特定のクラウド環境に縛られずに、動作する環境を選択できるのもメリットの一つではないでしょうか。
Feature
fn projectはOracleが開発を行っており、現在Open sourceでGithub上で活発に開発が行われています。
fn projectはいくつかのComponentで提供しており、主となるComponentはFunctionの実行環境の fn-server となります。これらのComponentをまとめたものを総称としてfn projectという名前を冠しているようです。
数多くのComponentがRepository上に公開されていますが、代表的なものをあげてみました。
サービス名 | 説明 | Repository URL |
---|---|---|
fn(fn-server) | fnはFunctionを実行・管理するためのServerアプリケーションである。Goで記述されており、起動するとDocker ImagをPullし実行。fn-serverプロセスが立ち上がる。 | https://github.com/fnproject/fn |
fdk | 各言語に対応したfn用のSDK。Go, node, Java, Python, Ruby等が現時点で確認できる言語となっている。 | https://github.com/fnproject/fdk-go 等・・ |
cli | fnをインストールすると同時に導入される。fn-serverへのDeployやFunctionの作成・管理等々のfnに関わる作業をすべてこちらのCLIから実行することができる。 | https://github.com/fnproject/cli |
lb | fn-serverをクラスタで利用する際に必要なLoad Balancerサービス。起動しているfn-serverプロセスを1ノードとして管理し、それぞれを追加・削除することでユーザーから要求されたFunctionの実行を分散する。 | https://github.com/fnproject/lb |
flow | Workflowを構成するサービス。単体で動作するFunctionをWorkflowに従ってまとめることでより複雑なタスクを実行することができる(と思う。未調査) | https://github.com/fnproject/flow |
ui | fn-serverのFunction実行状況や、Deploy済みFunction等々の情報を視覚化することができるWebサービス。管理用。 | https://github.com/fnproject/ui |
この中で最小限動作に必要なものは fn, fdk, cliの3つです。
それ以外のComponentはスケールが必要な環境の構築や本番運用時に必要になるツールのようです。ツールやサーバーはほぼGoで記述されており、コード量は比較的少ないように見えます。
Dockerベース
fnはDockerのContainer技術をコアにサービスが構成されており、Docker Image Repository(もしくはそれに準拠するRepository)を利用することができます。つまり、Docker CompatibilityであればECR等のサービスでも利用が可能です。
公式には以下のような特徴が記載されています。
- Open Source
- Native Docker: use any Docker container as your Function
- Supports all languages
- Run anywhere
- Public, private and hybrid cloud
- Import Lambda functions and run them anywhere
- Easy to use for developers
- Easy to manage for operators
- Written in Go
- Simple yet powerful extensibility
興味深いのがLambdaのConvert機能を有している点です。コマンドでLambdaのARNを指定するとfnのFunctionへの変換が可能とのこと(どこまで可能なのか試してみたい)
fnの目指すところ、fnの基本的な考え方や機能については Medium - 8 Reasons why we built the Fn Project に詳しく記載されています。
必要な最低限のスペック
Dockerのバージョンなど最低限必要なサービスやスペックは以下の通りです。
- Docker 17.10.0-ce or later installed and running
- A Docker Hub account (Docker Hub) (or other Docker-compliant registry)
- Log Docker into your Docker Hub account:
docker login
dockerのバージョンは 17.10.0-ce
以降のバージョンが必要です。そのため、これ以下のバージョンでは動作しません。
FaaSにおいて実行すべきFunctionのQueueingのためのサービス、Functionのメタ情報を格納するためのデータベースが必須となっていますが、fnの場合はローカルで動作するデータベースが組み込まれているため(SQLiteやインメモリキュー)、ローカルでの実行は簡単です。
Try fn
fnではFDKと呼ばれるSDKが提供されており、いくつかの言語に対応している。それぞれの言語のTutorialがあり、シンプルなFunctionを作成するだけならばドキュメントに沿っていくだけでコードの記述、Deploy、実行までが可能です。Hello World Functionの作成から実行までを確認してみます。
First Function
fnはSQLiteとインメモリのキューイングによりローカルで動作させることが可能です。
前提条件
- DockerがインストールされておりDaemonが起動している状態
- macOS Sierraで検証
Install
fn-cliを導入。
$ brew install fn
Macの場合は brew
がお手軽。Linuxの場合やbrewが実行できない場合は以下のコマンドで fn
をインストールします。
$ curl -LSs https://raw.githubusercontent.com/fnproject/cli/master/install | sh
https://github.com/fnproject/fn#install-cli-tool
Start fn-server
Functionはfn-serverによって管理され実行されます。
fn-server自体もDocker Imageから起動されるコンテナであり、Imageが存在しない場合は起動と共にfn-serverのImageをPullした上で起動されます。
$ fn start time="2018-10-19T01:59:48Z" level=info msg="Registering container driver 'docker'" time="2018-10-19T01:59:48Z" level=info msg="Registering log provider 's3'" time="2018-10-19T01:59:48Z" level=info msg="Registering data store provider 'sql'" time="2018-10-19T01:59:48Z" level=info msg="Registering log provider 'sql'" time="2018-10-19T01:59:48Z" level=info msg="Registering sql helper 'mysql'" time="2018-10-19T01:59:48Z" level=info msg="Registering sql helper 'postgres'" time="2018-10-19T01:59:48Z" level=info msg="Registering sql helper 'sqlite'" time="2018-10-19T01:59:48Z" level=info msg="Setting log level to" level=info time="2018-10-19T01:59:48Z" level=info msg="mysql does not support sqlite3" time="2018-10-19T01:59:48Z" level=info msg="postgres does not support sqlite3" time="2018-10-19T01:59:48Z" level=info msg="mysql does not support sqlite3" time="2018-10-19T01:59:48Z" level=info msg="postgres does not support sqlite3" time="2018-10-19T01:59:48Z" level=info msg="Connecting to DB" url=/app/data/fn.db time="2018-10-19T01:59:48Z" level=info msg="datastore dialed" datastore=sqlite3 max_idle_connections=256 url="sqlite3:///app/data/fn.db" time="2018-10-19T01:59:48Z" level=info msg="mysql does not support sqlite3" time="2018-10-19T01:59:48Z" level=info msg="postgres does not support sqlite3" time="2018-10-19T01:59:48Z" level=info msg="agent starting cfg={MinDockerVersion:17.10.0-ce DockerNetworks: DockerLoadFile: FreezeIdle:50ms EjectIdle:1s HotPoll:200ms HotLauncherTimeout:1h0m0s AsyncChewPoll:1m0s MaxResponseSize:0 MaxLogSize:1048576 MaxTotalCPU:0 MaxTotalMemory:0 MaxFsSize:0 PreForkPoolSize:0 PreForkImage:busybox PreForkCmd:tail -f /dev/null PreForkUseOnce:0 PreForkNetworks: EnableNBResourceTracker:false MaxTmpFsInodes:0 DisableReadOnlyRootFs:false DisableTini:false DisableDebugUserLogs:false IOFSEnableTmpfs:false IOFSAgentPath:/iofs IOFSMountRoot:/Users/komurohiraku/.fn/iofs IOFSOpts:}" time="2018-10-19T01:59:48Z" level=info msg="no docker auths from config files found (this is fine)" error="open /root/.dockercfg: no such file or directory" time="2018-10-19T01:59:48Z" level=info msg="available memory" cgroupLimit=9223372036854771712 headRoom=268435456 totalMemory=1688129536 time="2018-10-19T01:59:48Z" level=info msg="sync and async ram reservations" availMemory=1419694080 ramAsync=1135755264 ramAsyncHWMark=908604211 ramSync=283938816 time="2018-10-19T01:59:48Z" level=info msg="available cpu" availCPU=2000 totalCPU=2000 time="2018-10-19T01:59:48Z" level=info msg="sync and async cpu reservations" cpuAsync=1600 cpuAsyncHWMark=1280 cpuSync=400 time="2018-10-19T01:59:48Z" level=info msg="Fn serving on `:8080`" type=full ______ / ____/___ / /_ / __ \ / __/ / / / / /_/ /_/ /_/ v0.3.566
これでFunctionをDeployし実行する環境が整いました。検証時のfnのバージョンは v0.3.566
になります。続いてFunctionの方を作成していきましょう。
fn-server側の動作ログも確認したいので、こちらの起動した画面はそのままにしておいてください。
Create Initial function from template
対応言語を指定するだけで、その言語で記述されたFunctionの雛形を作成できます。Getting Startedに従って実行してみます。
$ fn init --runtime go dummy
上記コマンドはGoのFDKを利用したサンプルアプリケーションが生成されます。Function名はdummy
を指定しました。dummy/
が作成され必要なファイルが作成されます。
$ tree dummy/ dummy/ ├── Gopkg.toml ├── func.go └── func.yaml
Functionの実装は func.go
に。Functionの設定メタデータは func.yaml
に記載されています。
schema_version: 20180708 name: dummyfunc version: 0.0.1 runtime: go entrypoint: ./func format: http-stream
versionはdeploy実行ごとに0.0.1ずつインクリメントされていく模様です。Application名と同じにしてしまうとちょっと後半に困るので name
のみ dummyfunc
に変更しました。
Edit Sample Function
生成されたFunctionの内容を確認してみます。
package main import ( "context" "encoding/json" "fmt" "io" fdk "github.com/fnproject/fdk-go" ) func main() { fdk.Handle(fdk.HandlerFunc(myHandler)) } type Person struct { Name string `json:"name"` } func myHandler(ctx context.Context, in io.Reader, out io.Writer) { p := &Person{Name: "World"} json.NewDecoder(in).Decode(p) msg := struct { Msg string `json:"message"` }{ Msg: fmt.Sprintf("Hello %s", p.Name), } json.NewEncoder(out).Encode(&msg) }
fdk-go
を利用しています。所謂Hello Worldの簡単なFunctionコードのようです。goについてはあまり詳しくないのですがなんとなくやっていることが分かりそうです。
type Person struct { Name string `json:"name"` }
jsonの name
キーを取得してPerson.Nameに値を入力するようです。
p := &Person{Name: "World"}
入力値に name
キーを持ったJsonが指定されている場合はJsonの値を、存在しない場合は "World"
という文字列が指定されると読めます。
もう一つの Gopkg.toml
というファイルはGoの依存関係管理ツールの設定ファイルのようです。Developers.IO - Goオフィシャルチーム作成の依存関係管理ツール dep を試してみた
Deploy Function
Docker RepositoryへのImageのPushをスキップしてLocalに直接FunctionをDeployできます。これによりDocker Repositoryアカウントなどを必要とせずに簡単にFunctionを実行することができます。
$ fn --verbose deploy --app dummy --local Deploying dummy to app: dummy Bumped to version 0.0.2 Building image dummy:0.0.2 FN_REGISTRY: FN_REGISTRY is not set. Current Context: No context currently in use. Sending build context to Docker daemon 5.12kB Step 1/10 : FROM fnproject/go:dev as build-stage dev: Pulling from fnproject/go ff3a5c916c92: Already exists f32d2ea73378: Pull complete 3bdfb30a4c89: Pull complete 6487ee6212c5: Pull complete 074903419fc0: Pull complete 3db945ee2177: Pull complete Digest: sha256:6ebffaea00a2f53373c68dd52e0df209d7e464d691db0d52b31060d06df8e839 Status: Downloaded newer image for fnproject/go:dev ---> fac877f7d14d Step 2/10 : WORKDIR /function ---> Running in 9af29a96f8bb Removing intermediate container 9af29a96f8bb ---> fa81f6a83538 Step 3/10 : RUN go get -u github.com/golang/dep/cmd/dep ---> Running in 7254aebcb478 Removing intermediate container 7254aebcb478 ---> 28053ba3fdcb Step 4/10 : ADD . /go/src/func/ ---> f255d62a2e97 Step 5/10 : RUN cd /go/src/func/ && dep ensure ---> Running in e9a06a1092ba Removing intermediate container e9a06a1092ba ---> a325e3f6bbc1 Step 6/10 : RUN cd /go/src/func/ && go build -o func ---> Running in 6c8085d20384 Removing intermediate container 6c8085d20384 ---> dbb08c9e6fb6 Step 7/10 : FROM fnproject/go latest: Pulling from fnproject/go 1eae7a7426b0: Pull complete 7a855df78530: Pull complete Digest: sha256:8e03716b576e955c7606e4d8b8748c0f959a916ce16ba305ab262f042562340f Status: Downloaded newer image for fnproject/go:latest ---> 76aed4489768 Step 8/10 : WORKDIR /function ---> Running in 019de7b9bfc7 Removing intermediate container 019de7b9bfc7 ---> bea8b592cfbd Step 9/10 : COPY --from=build-stage /go/src/func/func /function/ ---> 11e135a40c50 Step 10/10 : ENTRYPOINT ["./func"] ---> Running in ad71de97bb06 Removing intermediate container ad71de97bb06 ---> c02608a82b7d Successfully built c02608a82b7d Successfully tagged dummy:0.0.2 Updating function dummy using image dummy:0.0.2... Successfully created app: dummy Successfully created function: dummy with dummy:0.0.2
--verbose
指定のためかなり詳細に出ていますが、依存するImageをPullしながらfunctionの実行コードを含んだImageを作成し、fn-server側へ登録しています。
Execute Function
Functionの実行にはいくつかあります。
CLIで実行する
fnコマンドのinvoke
でDeployしたFunctionを実行します。
DeployしたApplication名とFunctionの設定メタデータに記載したFunction名を指定します。
$ fn invoke dummy dummyfunc {"message":"Hello World"}
invokeの後ろの引数はDeployしたアプリケーション名, メタデータに記述したFunction名 になります。
実行引数を付与してFunctionを実行する
Functionの実行時に実行引数を付与して任意のパラメータをFunctionにわたすことができます。
$ echo -n '{"name":"KOMURO"}' | fn invoke dummy dummyfunc --content-type application/json {"message":"Hello KOMURO"}
パイプで渡すことができるようです。
HTTPメソッドで実行する
DeployされたFunctionは特定のエンドポイントを持ちます。そのため、このエンドポイントを通してHTTPメソッドでFunctionを呼び出すことができます。こちらも実行引数をつけて呼び出しが可能です。
$ curl -H "Content-Type: application/json" -d '{"name":"kom"}' -X GET 'http://localhost:8080/t/dummy/dummy-trigger' {"message":"Hello kom"}
Content-Typeに application/json
を指定してInputにJSONを指定しています。起動しているfn-serverの方では以下のログが出力されています。
time="2018-10-19T05:10:38Z" level=info msg="starting call" action="server.handleHTTPTriggerCall)-fm" appName=dummy app_id=01CT53JT4XNG8G00GZJ0000001 container_id=01CT5BQ223NG8G00GZJ000000M fn_id=01CT541M8DNG8G00GZJ0000007 id=01CT5BQ223NG8G00GZJ000000K triggerSource=/dummy-trigger
trigger経由で実行されているのが分かります。
ちなみにエンドポイント情報は以下のコマンドで取得可能です。
$ fn list triggers dummy FUNCTION NAME TYPE SOURCE ENDPOINT dummyfunc dummy-trigger http /dummy-trigger http://localhost:8080/t/dummy/dummy-trigger
注意事項
fnのバージョンアップによって、メタデータに明示的にTriggerという項目を追加しないとHTTPのエンドポイントが追加されなくなったようです。現在、fn-serverのREADMEの修正が追いついていないようで、READMEは参照にするとコケますのでお気をつけください。
エンドポイントを設定させるために func.yaml
を修正します。
triggers: - name: dummy-trigger type: http source: /dummy-trigger
再度Deployし直すと、HTTPで呼び出せるエンドポイントが作成されます。
以下が以前のバージョンと比べると大きな変更点です。
routes
というコマンドが存在しない- Triggerという概念が追加されている
元々 routes というコマンドが存在しエンドポイントの情報が確認できましたが(v0.3.504あたり)現在はコマンドがなくなっています。さらにREADMEの修正が追いついておらず、実装やHELPを見たほうが早いかもしれません。今のところ(2018/10/19現在)はTutorial(Go)のマニュアルが正しそうです。
まとめ
最小限のFunctionを作成しつつサーバーの起動からFunctionのDeploy、実行までを一通り体感してみました。ローカルで動作させるためのサービスやアプリケーションが組み込まれているため、準備にさほど時間がかからずに体感することができました。
実際にこれができるからイコールで本番適用できるわけではないのですが、検証までの時間をとても短くできることはとても良いかと思います。今後は実際運用できるレベルの構成が可能かなどの検証を引き続き行っていきます。
ただ、困ったことにコマンドを含めて様々な破壊的変更も多く入っているようで、先週まで動いていたものが動かない、といった状況もちょいちょい出てきており、このあたりはまだまだ完成には遠いのかなという印象です。そういったところも含め今後もしっかりウォッチしていこうかと思います。